-- EngineeringManualM
-- Created by dengc Sep/9/2016
-- 工程学手册模块

module("EngineeringManualM", package.seeall);

local NO_RELY_ON_ITEM = -1;     -- 没有所依赖项

local MANUAL_ID = 7509;         -- 工程学手册id

-- 工程学手册信息
local emTable = {};

local emManualTable = {};
local emTypeTable = {};
local emBonusTable = {};
local emInfluenceTable = {};

-- 所有的子规则
local rules = {};

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- engineering_manual.csv表
    local engineering_manual_CSV = get_engineering_manual_CSV();
    for i = 1, #engineering_manual_CSV do
        local id = engineering_manual_CSV[i].id;
        emTable[id] = engineering_manual_CSV[i];

        local manualType = engineering_manual_CSV[i].type;
        emTypeTable[manualType] = emTypeTable[manualType] or {};
        emTypeTable[manualType][id] = engineering_manual_CSV[i];

        local dungeonId  = engineering_manual_CSV[i].dungeon;
        emManualTable[dungeonId] = emManualTable[dungeonId] or {};
        emManualTable[dungeonId][id] = engineering_manual_CSV[i];
    end
    engineering_manual_CSV = {};

    -- engineering_manual_bonus.csv表
    local engineering_manual_bonus_CSV = get_engineering_manual_bonus_CSV();
    for i = 1, #engineering_manual_bonus_CSV do
        local bonusId = engineering_manual_bonus_CSV[i].bonus_id;
        emBonusTable[bonusId] = engineering_manual_bonus_CSV[i];
    end
    engineering_manual_bonus_CSV = {};

    -- engineering_manual_influence.csv表
    local engineering_manual_influence_CSV = get_engineering_manual_influence_CSV();
    for i = 1, #engineering_manual_influence_CSV do
        local classId = engineering_manual_influence_CSV[i].class_id;
        emInfluenceTable[classId] = engineering_manual_influence_CSV[i];
    end
    engineering_manual_influence_CSV = {};
end

-- 模块初始化
function init()
    if not _init then
        -- 载入配置
        loadCsv();

        -- 载入所有的规则处理子模块
        rules = LOAD_PATH("game/logic/module/engineering_manual");
    end

    EventMgr.removeAll("EngineeringManualM");

    -- 关注开始地牢事件
    EventMgr.register("EngineeringManualM", event.START_DUNGEON, function(layer)
        whenStartDungeon();
    end);

    -- 关注进行地牢事件
    EventMgr.register("EngineeringManualM", event.CONTINUE_DUNGEON, function(layer)
        whenStartDungeon();
    end);

    -- 关注玩家离开地牢的事件
    EventMgr.register("EngineeringManualM", event.LEAVE_DUNGEON, function()
        whenLeaveDungeon();
    end);

    -- 关注获得物品的事件
    EventMgr.register("EngineeringManualM", event.BONUS_ITEM, function(args)
        whenDoItemBonus(args);
    end);

    -- 关注使用物品的事件
    EventMgr.register("EngineeringManualM", event.USE_PROPERTY, function(args)
        whenUseProperty(args);
    end);
end

-- 进入地牢
whenStartDungeon = function(layer)
    -- 初始化手册信息
    local dungeonId = DungeonM.getDungeonId();

    initManual(dungeonId);

    EventMgr.fire(event.MANUAL_APPLY_EQUIP_PROP);
end

-- 离开地牢
whenLeaveDungeon = function()
    -- 删除手册数据
    ME.user.dbase:delete("engineering_manual");
end

-- 获得物品
whenDoItemBonus = function(args)
    local classId = args["classId"];
    if classId ~= MANUAL_ID then
        return;
    end

    -- 获得工程学手册时，根据出战冈布奥直接学习满指定知识点
    local petId = ME.user.dbase:query("active_pet", 0);
    local knowledges = FormulaM.invoke("GET_PET_PROMOTE_ENGINNERING", petId);

    local dungeonId = DungeonM.getDungeonId();
    if DungeonAreaM.query(dungeonId, "type") == "child" then
        dungeonId = DungeonAreaM.getParentId(dungeonId);
    end
    local knowledgeId, maxLevel;

    for _, knowledgeId in ipairs(knowledges) do
        if dungeonId == query(knowledgeId, "dungeon") then
            maxLevel = query(knowledgeId, "max_level");
            rewardKnowledgePoint(knowledgeId, maxLevel);
        end
    end
end

-- 使用物品
whenUseProperty = function(args)
    local classId = args.classId;
    if not classId then
        return;
    end

    applyProperty(classId, args.pos);
end

-- 查询知识点信息
function query(id, path)
    if not emTable[id] then
        return nil;
    end

    if path == nil or path == "" then
        return emTable[id];
    end

    return emTable[id][path];
end

-- 按手册类型查找信息
function queryType(type)
    if not emManualTable then
        return nil;
    end

    return emManualTable[type];
end

-- 查询学科数据
function querySubject(subject, id, path)
    if not emTypeTable[subject] then
        return nil;
    end

    if not id then
        return emTypeTable[subject];
    end

    if not emTypeTable[subject][id] then
        return nil;
    end

    if path == nil or path == "" then
        return emTypeTable[subject][id];
    end

    return emTypeTable[subject][id][path];
end

-- 查询奖励
function queryBonus(id, path)
    if not emBonusTable[id] then
        return nil;
    end

    if path == nil or path == "" then
        return emBonusTable[id];
    end

    return emBonusTable[id][path];
end

-- 查询受影响的配置
function queryInfluence(classId, path)
    if not emInfluenceTable[classId] then
        return nil;
    end

    if path == nil or path == "" then
        return emInfluenceTable[classId];
    end

    return emInfluenceTable[classId][path];
end

-- 获取子规则
function queryRule(rule)
    if rules[rule] == "TBL" then
        rules[rule] = LOAD_RUN("game/logic/module/engineering_manual/" .. rule);
    end

    return rules[rule];
end

-- 初始化手册数据
function initManual(dungeonId)
    local manualInfo = {};

    -- 如果是子区域
    if DungeonAreaM.query(dungeonId, "type") == "child" then
        dungeonId = DungeonAreaM.getParentId(dungeonId);
    end

    -- 获取能学习的学科
    local typeInfo = emManualTable[dungeonId];
    if not typeInfo then
        ME.user.dbase:set("engineering_manual", {});
        return;
    end

    -- 原来还有数据，用原来的
    if ME.user.dbase:query("engineering_manual") then
        return;
    end

    for id, knowledgeInfo in pairs(typeInfo) do
        if knowledgeInfo["last_id"] == NO_RELY_ON_ITEM then
            manualInfo[id] = 1;
        end
    end

    ME.user.dbase:set("engineering_manual", manualInfo);
end

-- 是否拥有工程学手册
function isHaveEngineeringManual()
    return ItemM.getAmount(ME.user, MANUAL_ID) > 0
end

-- 工程学手册能否开启
function canOpen()
    local dungeonId = DungeonM.getDungeonId();

    -- 如果是子区域
    if DungeonAreaM.query(dungeonId, "type") == "child" then
        dungeonId = DungeonAreaM.getParentId(dungeonId);
    end

    if not emManualTable[dungeonId] then
        return false;
    end

    return true;
end

-- 检查知识点是否可以无限学习
function isUnlimited(id)
    local lastId = query(id, "last_id");
    if lastId == NO_RELY_ON_ITEM then
        return true;
    end

    return false;
end

-- 检查某一知识点是否学习过
function checkIsLearn(id)
    if 0 == getKnowledgeLevel(id) then
        return false;
    end

    return true;
end

-- 是否可以跨级学习
function canIgnoreLast(id)
    -- 各个子规则额外的检查
    local ruleName = query(id, "rule");
    if ruleName then
        local rule = queryRule(ruleName);
        if rule and rule.canIgnoreLast and rule.canIgnoreLast(id) then
            return 1;
        end
    end

    return 0;
end

-- 检查能否学习
-- @param id         工程学手册的id
-- @param ignoreCost 忽略消耗的检查
function checkCanLearn(id, ignoreCost, ignoreLast)
    -- 不是对应的手册类型
    local dungeonId = query(id, "dungeon");
    local curDungeonId = DungeonM.getDungeonId();

    -- 如果是子区域
    if DungeonAreaM.query(curDungeonId, "type") == "child" then
        curDungeonId = DungeonAreaM.getParentId(curDungeonId);
    end

    if dungeonId ~= curDungeonId then
        return false;
    end

    -- 上一个知识点还没有开启
    local lastId = query(id, "last_id");
    ignoreLast = ignoreLast or false;
    if not ignoreLast then
        if not checkIsLearn(lastId) and lastId ~= NO_RELY_ON_ITEM then
            return false;
        end
    end

    -- 达到了最大等级
    if isAchieveMaxLevel(id) then
        return false;
    end

    -- 负担不起消耗
    if not ignoreCost then
        if not checkCanCost(id) then
            return false;
        end
    end

    -- 各个子规则额外的检查
    local ruleName = query(id, "rule");
    if ruleName then
        local rule = queryRule(ruleName);
        if rule and rule.check then
            if true ~= rule.check(id) then
                return false;
            end
        end
    end

    return true;
end

-- 获取工程学手册所有能够学习的知识点
function getAllCanLearn(ignoreCost)
    local arr = {};

    local manualInfo = ME.user.dbase:query("engineering_manual") or {};
    for id, _ in pairs(manualInfo) do
        if EngineeringManualM.checkCanLearn(id, ignoreCost) then
            table.insert(arr, id);
        end
    end

    return arr;
end

-- 获取工程学手册某个学科所有能够学习的知识点
-- @param subject       学科类型
-- @param ignoreCost    是否忽略学习消耗
-- @param ignoreUnlimit 是否排除可以无限学习的知识点
function getSubjectAllCanLearn(subject, ignoreCost, ignoreUnlimit)
    local knowledgesInfo = emTypeTable[subject];
    if not knowledgesInfo then
        return {};
    end

    local knowledges = {};
    for id, _ in pairs(knowledgesInfo) do
        table.insert(knowledges, id);
    end

    -- 找到所有能学习的知识点
    local arr = {};
    for _, id in ipairs(knowledges) do
        -- 如果需要排除可以无限学习的知识点
        -- 并在可学习的范围内选取
        if (not ignoreUnlimit or not isUnlimited(id)) and
            EngineeringManualM.checkCanLearn(id, ignoreCost) then
            table.insert(arr, id);
        end
    end

    -- 排序下
    table.sort(arr);

    return arr;
end

-- 某个知识点是否达到了最大等级
function isAchieveMaxLevel(id)
    if getKnowledgeLevel(id) == query(id, "max_level") and
        query(id, "last_id") ~= NO_RELY_ON_ITEM then
        return true;
    end

    return false;
end

-- 学习知识点
-- @param id         工程学手册知识点id
-- @param ignoreCost 是否忽略学习消耗
function learnKnowledgePoint(id, ignoreCost, ignoreLast)
    -- 检查能否学习
    ignoreLast = ignoreLast or false;
    if not checkCanLearn(id, ignoreCost, ignoreLast) then
        return false;
    end

    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return false;
    end

    -- 消耗
    if not ignoreCost then
        doCost(id);

        -- 标记学习过手册
        DungeonM.setDungeonMixedField("has_learn_manual", 1);
    end

    -- 不可无限学习的知识点，等级加1
    if not isUnlimited(id) then
        manualInfo[id] = getKnowledgeLevel(id) + 1;
    end

    -- 学习效果
    doApply(id, manualInfo[id]);

    -- 抛出学习成功事件
    -- 免费学习时不需要提示
    EventMgr.fire(event.LEARN_MANUAL_SUCCEED, { ["id"] = id, ["is_alert"] = iif(ignoreCost, false, true), });

    return true;
end

-- 奖励知识点
function rewardKnowledgePoint(id, level, extra)
    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return;
    end

    -- 不能超过最大等级
    local maxLevel = query(id, "max_level");
    if not maxLevel then
        return;
    end

    level = (level <= maxLevel) and level or maxLevel;

    -- 循环学习
    for i = 1, level do
        learnKnowledgePoint(id, true, true);
    end

    -- 抛出事件
    EventMgr.fire(event.REWARD_ENGINEERING_KNOWLEDGE, {["id"] = id, ["level"] = level, ["extra"] = extra, });
end

-- 学习效果
function doApply(id, level)
    local bonusId = query(id, "bonus_id");
    if bonusId == "" then
        -- 所有知识点的间接奖励都可能影响装备属性，刷新一下
        refreshManualProps();

        return;
    end

    local bonusLevel = "lv" .. level;
    local bonus = table.deepcopy(queryBonus(bonusId, bonusLevel));

    -- 遍历奖励列表，给予奖励
    for key, info in pairs(bonus) do
        local mod = queryRule(key);
        if mod and mod.apply then
            local value = table.deepcopy(info);
            mod.apply(id, value);
        else
            -- 为发现问题，先断言下
            assert(false, string.format("没有找到子模块：%s\n", key));
        end
    end

    -- 如果配置了对道具的额外影响，需要被刷新下属性
    local addon = queryBonus(bonusId, "property_addition");
    if sizeof(addon) > 0 then
        PropM.refreshEffect(ME.user, "property_engineeing_manual");
    end

    -- 所有知识点的间接奖励都可能影响装备属性，刷新一下
    refreshManualProps();
end

-- 道具起效
function applyProperty(classId, pos)
    DungeonLogM.addLog(string.format("工程学手册对道具%d的额外效果加成", classId));

    local args = queryInfluence(classId, "args") or {};
    for _, info in ipairs(args) do
        for id, bonusId in pairs(info) do
            repeat
                if getKnowledgeLevel(id) <= 0 then
                    break;
                end

                local target = getTarget(pos, bonusId);
                if not target then
                    break;
                end

                local bonusLevel = "lv1";
                local bonus = queryBonus(bonusId, bonusLevel) or {};
                local keys = table.keys(bonus);

                table.sort(keys);
                for i = 1, #keys do
                    local key = keys[i];
                    local value = table.deepcopy(bonus[key]);

                    local mod = queryRule(key);

                    if mod and mod.useProperty then
                        mod.useProperty(target, bonusId, value);
                    else
                        -- 断言下方便检查出问题
                        assert(false, string.format("找不到子模块：%s\n", key));
                    end
                end
            until true;
        end
    end
end

-- 获取作用目标
function getTarget(pos, bonusId)
    -- 没有位置
    if not pos or pos <= 0 then
        return ME.user;
    end

    -- 查看目标
    local dbase = queryBonus(bonusId, "dbase") or {};
    local targetType = dbase["target"];
    if not targetType or targetType ~= PROPERTY_ENEMY then
        return ME.user;
    end

    -- 获取目标
    local grid = DungeonM.getGridByPos(pos);
    local monster = grid.monster;

    -- 目标不正确
    if not grid:isOpened() or not grid:isMonster() or
        not monster or monster:isDead() then
        return nil;
    end

    return monster;
end

-- 获取召唤兽的属性加成
function fetchSummonProps(summon)
    local props = {};

    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return {};
    end

    for id, level in pairs(manualInfo) do
        repeat
            if level == 0 then
                break;
            end

            local bonusId = query(id, "bonus_id");
            if not bonusId or bonusId == "" then
                break;
            end

            local bonusLevel = "lv" .. level;
            local bonus = queryBonus(bonusId, bonusLevel);
            if not bonus then
                break;
            end

            local prop = bonus["summon_prop"];
            if prop then
                if type(prop[1]) ~= "table" then
                    prop = { prop };
                end

                props = table.append(props, prop);
            end

            prop = bonus["summon_prop_" .. summon.classId];
            if prop then
                if type(prop[1]) ~= "table" then
                    prop = { prop };
                end

                props = table.append(props, prop);
            end
        until true;
    end

    return props;
end

-- 获取工程学手册赋予玩家的属性
function fetchProps()
    local props = {};

    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return {};
    end

    for id, level in pairs(manualInfo) do
        repeat
            if level == 0 then
                break;
            end

            local bonusId = query(id, "bonus_id");
            if not bonusId or bonusId == "" then
                break;
            end

            local bonusLevel = "lv" .. level;
            local bonus = queryBonus(bonusId, bonusLevel);
            if not bonus then
                break;
            end

            local prop = bonus["prop"];
            if prop then
                if type(prop[1]) ~= "table" then
                    prop = { prop };
                end

                props = table.append(props, prop);
            end
        until true;
    end

    return props;
end

-- 获取工程学手册赋予阿瓦隆巨炮的技能
function fetchAvalonSkills()
    local skills = {};

    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return {};
    end

    for id, level in pairs(manualInfo) do
        repeat
            if level == 0 then
                break;
            end

            local bonusId = query(id, "bonus_id");
            if not bonusId or bonusId == "" then
                break;
            end

            local bonusLevel = "lv" .. level;
            local bonus = queryBonus(bonusId, bonusLevel);
            if not bonus then
                break;
            end

            local info = bonus["avalon_cannon"];
            if not info then
                break;
            end

            table.insert(skills, info[1]);
        until true;
    end

    return skills;
end

-- 获取对道具的额外加成属性
function fetchPropertyAddProps()
    local props = {};

    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo or type(manualInfo) ~= "table" then
        return {};
    end

    for id, level in pairs(manualInfo) do
        repeat
            if level == 0 then
                break;
            end

            local bonusId = query(id, "bonus_id");
            if not bonusId or bonusId == "" then
                break;
            end

            local propertyAddition = queryBonus(bonusId, "property_addition");
            if sizeof(propertyAddition) <= 0 then
                break;
            end

            local prop = propertyAddition["prop"];
            if prop then
                if type(prop[1]) ~= "table" then
                    prop = { prop };
                end

                props = table.append(props, prop);
            end
        until true;
    end

    return props;
end

-- 刷新工程学手册的加成效果
function refreshManualProps()
    -- 刷新玩家身上的属性
    PropM.refreshEffect(ME.user, "engineering_manual");

    -- 刷新对道具的额外加成效果
    PropM.refreshEffect(ME.user, "property_engineeing_manual");

    -- 刷新下装备赋予的属性
    PropM.refreshEffect(ME.user, "equip");

    -- 抛出附加装备属性的事件
    EventMgr.fire(event.MANUAL_APPLY_EQUIP_PROP);
end

-- 获取工程学手册对某一项目的加成效果
-- @param path          取该路径下的信息
-- @param isForDesc     用于描述，默认为false
-- @param isSort        描述排序
function getManualAddition(itemId, path, isForDesc, isSort)
    local additionProps = {};

    isForDesc = isForDesc or false;

    local args = queryInfluence(itemId, "args") or {};
    for _, info in ipairs(args) do
        for key, value in pairs(info) do
            repeat
                local level = getKnowledgeLevel(key);
                if level <= 0 then
                    break;
                end

                local bonusLevel = "lv" .. level;

                -- 与手册等级不挂钩的物品等级始终默认为1
                if queryInfluence(itemId, "not_relay_on") == 1 or sizeof(queryBonus(value, bonusLevel)) <= 0 then
                    bonusLevel = "lv1";
                end

                local bonus = queryBonus(value, bonusLevel) or {};
                local props = bonus[path] or {};

                if sizeof(props) > 0 and type(props[1]) ~= "table" then
                    props = { props, };
                end

                local weight;
                for _, prop in ipairs(props) do
                    if isSort == 1 then
                        additionProps[key] = additionProps[key] or {};
                        prop = bonus[path];
                        weight = queryBonus(value, "weight") or 1;
                        table.insert(additionProps[key], {prop[1], prop[2], prop[3], prop[4], weight});
                    elseif isForDesc then
                        additionProps[key] = additionProps[key] or {};
                        table.insert(additionProps[key], bonus[path]);
                    else
                        table.insert(additionProps, bonus[path]);
                    end
                end
            until true;
        end
    end

    return additionProps;
end

-- 能否负担起消耗
function checkCanCost(id)
    local cost = query(id, "cost");
    if not cost then
        return true;
    end

    local ok = true;
    for key, amount in pairs(cost) do
        if type(key) == "string" then
            -- 属性
            ok = ME.user.dbase:query(key, 0) >= amount;
    elseif type(key) == "number" then
            -- 物品
            ok = ItemM.getAmount(ME.user, key) >= amount;
        else
            -- 暂无其他消耗
            ok = false;
        end

        if not ok then
            trace("EngineeringManualM", "%s不足，不能学习。", tostring(key));
            return ok;
        end
    end

    return ok;
end

-- 学习消耗
function doCost(id)
    -- 消耗
    local cost = query(id, "cost");
    if not cost then
        return;
    end

    for key, amount in pairs(cost) do
        if type(key) == "number" then
            ItemM.costAmount(ME.user, key, amount);
        elseif type(key) == "string" then
            ME.user:costAttrib(key, amount);
        end
    end
end

-- 获取奖励描述
-- 用于工程学手册的二级界面显示
function getBonusDesc(id)
    local bonusId = query(id, "bonus_id");
    if bonusId == "" then
        return;
    end

    -- 显示下一级的效果，如果满级，那么显示满级的效果
    local level = getKnowledgeLevel(id);
    if not isAchieveMaxLevel(id) and not isUnlimited(id) then
        level = level + 1;
    end

    local bonusLevel = "lv" .. level;

    -- 描述
    local desc = "";
    local bonus = queryBonus(bonusId, bonusLevel);
    for key, info in pairs(bonus) do
        local mod = queryRule(key);
        if mod and mod.desc then
            -- 获取描述
            local temp = mod.desc(id, info);
            if temp and temp ~= "" then
                desc = spliceLocStr(desc, temp);
            end
        end
    end

    return desc;
end

-- 获取附加属性的描述
-- 用于对装备的属性附加描述
function getEquipAdditionDesc(itemId)
    -- 增幅来源相同的属性整理在一起，并加上增幅项来源前缀
    -- 根据配置，来判断是否配置权重
    local isSort = queryInfluence(itemId, "is_sort") or 0;

    local arrangeProps = table.deepcopy(getManualAddition(itemId, "equip", true, isSort));
    if not arrangeProps then
        return;
    end

    -- 整理好了，那么开始写描述了
    local desc = "";
    local descList = {};
    for id, info in pairs(arrangeProps) do
        local temp = "";

        -- 先把来源写出来
        if query(id, "addition_desc") ~= "" then
            desc = desc .. query(id, "addition_desc") .. "\n";
            temp = temp .. query(id, "addition_desc") .. "\n";
        end

        -- 如果配置了权重，那么需要排序一下
        if isSort == 1 then
            table.sort(info, function(a, b) return a[5] < b[5]; end);
        end

        -- 遍历下，把来源相同的属性效果都写出来
        for _, prop in ipairs(info) do
            -- 匹配掉颜色信息
            local propDesc = PropM.getPropDesc(prop);
            propDesc = string.gsub(propDesc, "%[.-%]", "");

            desc = desc .. propDesc .. "\n";
            temp = temp .. propDesc .. "\n";
        end

        -- 来源不同的项目也应该排序下
        if isSort == 1 then
            local weight = query(id, "weight") or 1;
            table.insert(descList, {weight, temp});
        end
    end

    -- 最后排序下
    if isSort == 1 then
        table.sort(descList, function(a, b) return a[1] < b[1]; end);

        desc = "";
        for i = 1, #descList do
            desc = desc .. descList[i][2];
        end
    end

    return desc;
end

-- 获取附加效果的描述
-- 用于对道具的效果附加描述
function getItemAdditionDesc(itemId)
    -- 根据配置，来判断是否配置权重
    local isSort = queryInfluence(itemId, "is_sort") or 0;

    local additionDesc = "";
    local descList = {};

    -- 获取描述
    local extraDesc = queryInfluence(itemId, "desc");
    if not extraDesc or sizeof(extraDesc) <= 0 then
        return additionDesc;
    end

    for id, desc in pairs(extraDesc) do
        local level = getKnowledgeLevel(id);
        if type(level) == "number" and level > 0 then
            local temp = "";

            -- 先把来源写出来
            additionDesc = spliceLocStr(additionDesc, query(id, "addition_desc") .. "\n");
            temp = spliceLocStr(temp, query(id, "addition_desc") .. "\n");

            -- 附加上描述
            additionDesc = spliceLocStr(additionDesc, desc .. "\n");
            temp = spliceLocStr(temp, desc .. "\n");

            if isSort then
                local weight = query(id, "weight") or 1;
                table.insert(descList, {weight, temp});
            end
        end
    end

    -- 最后排序下
    if isSort == 1 then
        table.sort(descList, function(a, b) return a[1] < b[1]; end);

        additionDesc = "";
        for i = 1, #descList do
            additionDesc = spliceLocStr(additionDesc, descList[i][2]);
        end
    end

    return additionDesc;
end

-- 指定关卡的工程学手册是否学满了
function isLearnAllKnowledge(dungeonId)
    -- 如果是子区域
    if DungeonAreaM.query(dungeonId, "type") == "child" then
        dungeonId = DungeonAreaM.getParentId(dungeonId);
    end

    -- 获取能学习的学科
    local typeInfo = emManualTable[dungeonId];
    if not typeInfo then
        return false;
    end

    for id, knowledgeInfo in pairs(typeInfo) do
        -- 如果有任何一个可升级项没满级
        if knowledgeInfo["last_id"] ~= NO_RELY_ON_ITEM and
            getKnowledgeLevel(id) < knowledgeInfo["max_level"] then
            return false;
        end
    end

    return true;
end

-- 某类手册是否学满了
function subjectOver(subject)
    local list = querySubject(subject);

    if not list then
        return false;
    end

    for id, knowledgeInfo in pairs(list) do
        -- 如果有任何一个可升级项没满级
        if knowledgeInfo["last_id"] ~= NO_RELY_ON_ITEM and
            getKnowledgeLevel(id) < knowledgeInfo["max_level"] then
            return false;
        end
    end

    return true;
end

-- 获取某一知识点的等级
function getKnowledgeLevel(id)
    local manualInfo = ME.user.dbase:query("engineering_manual");
    if not manualInfo then
        return 0;
    end

    local level = manualInfo[id];
    return tonumber(level);
end

-- 是否需要隐藏某一项
function needHideSubject(id)
    -- 各个子规则额外的检查
    local ruleName = query(id, "rule");
    if ruleName then
        local rule = queryRule(ruleName);
        if rule and rule.checkHideSubjc then
            return rule.checkHideSubjc(id);
        end
    end

    return false;
end

-- 工程学手册是否开启
function isOpen()
    -- 默认关闭
    return ME.user.dbase:query("open_manual") or 0;
end

-- 验证客户端/客户端更新GS上的数据
function updateManual(data)
    if not data then
        return;
    end

    local manualInfo = {};

    -- 更新所有的手册信息
    for id, value in pairs(data) do
        manualInfo[id] = value;
    end

    ME.user.dbase:set("engineering_manual", manualInfo);
end
